/*******************************************************************************
 * Copyright (c) 2010-2011 Robert Fraser                                       *
 *                                                                             *
 * Permission is hereby granted, free of charge, to any person obtaining a     *
 * copy of this software and associated documentation files (the "Software"),  *
 * to deal in the Software without restriction, including without limitation   *
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,    *
 * and/or sell copies of the Software, and to permit persons to whom the       *
 * Software is furnished to do so, subject to the following conditions:        *
 *                                                                             *
 *   - The above copyright notice and this permission notice shall be included *
 *     in all copies or substantial portions of the Software.                  *
 *                                                                             *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR  *
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,    *
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER      *
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING     *
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER         *
 * DEALINGS IN THE SOFTWARE.                                                   *
 *******************************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;

namespace zeal.util
{
    public enum LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL,
        MAX
    }

    public sealed class LogLine
    {
        public readonly DateTime time;
        public readonly LogLevel level;
        public readonly string header;
        public readonly string message;
        public readonly Exception exception;
        public readonly string function;
        public readonly string file;
        public readonly int? line;

        public LogLine(DateTime time, LogLevel level, string header, string message, Exception exception,
            string function, string file, int? line)
        {
            this.time = time;
            this.level = level;
            this.header = header;
            this.message = message;
            this.exception = exception;
            this.function = function;
            this.file = file;
            this.line = line;

            StringBuilder s = new StringBuilder();
            switch (level)
            {
                case LogLevel.DEBUG: s.Append("[DEBUG] "); break;
                case LogLevel.INFO: s.Append("[INFO] "); break;
                case LogLevel.WARNING: s.Append("[WARNING] "); break;
                case LogLevel.ERROR: s.Append("[ERROR] "); break;
                case LogLevel.FATAL: s.Append("[FATAL] "); break;
                default: throw new ArgumentOutOfRangeException();
            }
            s.AppendFormat("[{0}] ", header);
            if (null != function) s.AppendFormat("[{0}@{1}:{2}] ", function, file, line);
            if (null != message) s.Append(message);
            if (null != exception)
            {
                s.Append(exception.Message);
                s.Append(exception.StackTrace);
            }
            _toString = s.ToString();
        }

        private readonly string _toString;
        public override string ToString() { return _toString; }
    }

    public interface ILogListener
    {
        LogLevel minLevel { get; }
        void write(LogLine line);
        void attach(DateTime time);
        void setMetadata(string key, string value);
    }

    public struct Log
    {
        private readonly string _header;

        public Log(string header)
        {
            _header = header;
        }

        public void debug(string message, params object[] args)                { LogManager.log(LogLevel.DEBUG, _header, message, args); }
        public void debug(Exception e)                                         { LogManager.log(LogLevel.DEBUG, _header, e, null); }
        public void debug(Exception e, string message, params object[] args)   { LogManager.log(LogLevel.DEBUG, _header, e, message, args); }
        public void info(string message, params object[] args)                 { LogManager.log(LogLevel.INFO, _header, message, args); }
        public void info(Exception e)                                          { LogManager.log(LogLevel.INFO, _header, e, null); }
        public void info(Exception e, string message, params object[] args)    { LogManager.log(LogLevel.INFO, _header, e, message, args); }
        public void warning(string message, params object[] args)              { LogManager.log(LogLevel.WARNING, _header, message, args); }
        public void warning(Exception e)                                       { LogManager.log(LogLevel.WARNING, _header, e, null); }
        public void warning(Exception e, string message, params object[] args) { LogManager.log(LogLevel.WARNING, _header, e, message, args); }
        public void error(string message, params object[] args)                { LogManager.log(LogLevel.ERROR, _header, message, args); }
        public void error(Exception e)                                         { LogManager.log(LogLevel.ERROR, _header, e, null); }
        public void error(Exception e, string message, params object[] args)   { LogManager.log(LogLevel.ERROR, _header, e, message, args); }
        public void fatal(string message, params object[] args)                { LogManager.log(LogLevel.FATAL, _header, message, args); }
        public void fatal(Exception e)                                         { LogManager.log(LogLevel.FATAL, _header, e, null); }
        public void fatal(Exception e, string message, params object[] args)   { LogManager.log(LogLevel.FATAL, _header, e, message, args); }
    }

    public static class LogManager
    {
        public const string METADATA_PREFIX_ASSEMBLY_VERSION = "assembly:";
        public const string METADATA_KEY_SESSION_START = "started";

        public static Log getLog(string header)
        {
            return new Log(header);
        }

        public static Log getLog(Type type)
        {
            return new Log(type.Name);
        }

        [Conditional("TRACE")]
        public static void registerAssembly(Assembly asm)
        {
            object[] asmFileVersions = asm.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), true);
            if (asmFileVersions.Length == 0)
                return;
            AssemblyFileVersionAttribute version = asmFileVersions[0] as AssemblyFileVersionAttribute;
            string versionStr = null != version ? version.Version : "(unknown)";
            string fullName = asm.FullName;
            // ReSharper disable PossibleNullReferenceException
            string nameStr = fullName.Substring(0, fullName.IndexOf(','));
            // ReSharper restore PossibleNullReferenceException
            instance.internalSetMetadata(METADATA_PREFIX_ASSEMBLY_VERSION + nameStr, versionStr);
        }

        // Wrappers for internal methods
        [Conditional("TRACE")] public static void registerAssembly() { registerAssembly(Assembly.GetCallingAssembly()); }
        [Conditional("TRACE")] public static void log(LogLevel level, string header, string message, params object[] args) { instance.internalLog(level, header, message == null ? null : string.Format(message, args), null); }
        [Conditional("TRACE")] public static void log(LogLevel level, string header, Exception e, string message, params object[] args) { instance.internalLog(level, header, message == null ? null : string.Format(message, args), e); }
        [Conditional("TRACE")] public static void setMetadata(string key, string value) { instance.internalSetMetadata(key, value); }
        [Conditional("TRACE")] public static void attach(ILogListener listener) { instance.internalAttach(listener); }
        [Conditional("TRACE")] public static void detatch(ILogListener listener) { instance.internalDetatch(listener); }

        private static readonly Logger instance = new Logger();
        private sealed class Logger
        {
            private readonly List<ILogListener> _listeners = new List<ILogListener>();
            private LogLevel _minLevel = LogLevel.MAX;
            private readonly Dictionary<string, string> _globalMetadata = new Dictionary<string, string>();

            public void internalAttach(ILogListener listener)
            {
                lock (this)
                {
                    if (!_listeners.Contains(listener))
                        _listeners.Add(listener);
                    _minLevel = listener.minLevel < _minLevel ? listener.minLevel : _minLevel;
                    DateTime time = DateTime.Now.ToUniversalTime();
                    listener.attach(time.ToLocalTime());
                    listener.setMetadata(METADATA_KEY_SESSION_START, time.ToString());
                    foreach (KeyValuePair<string, string> metadata in _globalMetadata)
                    {
                        listener.setMetadata(metadata.Key, metadata.Value);
                    }
                }
            }
                        
            public void internalDetatch(ILogListener listener)
            {
                lock (this)
                {
                    int count = _listeners.Count;
                    LogLevel minLevel = LogLevel.MAX;
                    int i = 0;
                    while (i < count)
                    {
                        ILogListener l = _listeners[i];
                        if (l == listener)
                        {
                            _listeners.RemoveAt(i);
                        }
                        else
                        {
                            minLevel = l.minLevel < minLevel ? l.minLevel : minLevel;
                            i++;
                        }
                    }
                    _minLevel = minLevel;
                }
            }

            public void internalLog(LogLevel level, string header, string message, Exception exception)
            {
                if (_minLevel > level || _listeners.Count == 0)
                    return;

                // Get debug info
                string function = null;
                string file = null;
                int? line = null;
                #if DEBUG
                    StackFrame frame = new StackTrace(true).GetFrame(4);
                    string path = frame.GetFileName();
                    function = frame.GetMethod().Name;
                    file = Path.GetFileName(path);
                    line = frame.GetFileLineNumber();
                #endif
                
                write(new LogLine(DateTime.Now, level, header, message, exception, function, file, line));
            }

            public void internalSetMetadata(string key, string value)
            {
                lock(this)
                {
                    _globalMetadata[key] = value;
                    foreach (ILogListener listener in _listeners)
                        listener.setMetadata(key, value);
                    write(new LogLine(DateTime.Now, LogLevel.INFO, "METADATA", string.Format("{0} = {1}", key, value), null, null, null, null));
                }
            }

            private void write(LogLine line)
            {
                lock(this)
                {
                    foreach (ILogListener listener in _listeners)
                        if (listener.minLevel <= line.level)
                            listener.write(line);
                }
            }
        }
    }

    public sealed class DebugLogListener : ILogListener
    {
        private readonly LogLevel _minLevel;
        public DebugLogListener(LogLevel minLevel) { _minLevel = minLevel; }
        public LogLevel minLevel { get { return _minLevel; } }
        public void attach(DateTime time) { }
        public void setMetadata(string key, string value) { }
        public void write(LogLine line) { Debug.WriteLine(line.ToString()); }
    }

    public sealed class StreamLogListener : ILogListener, IDisposable
    {
        private readonly LogLevel _minLevel;
        private readonly TextWriter _writer;

        public StreamLogListener(LogLevel level, string filename) :
            this(level, new FileStream(filename, FileMode.Append)) { }
        public StreamLogListener(LogLevel level, Stream stream)
        {
            _minLevel = level;
            _writer = new StreamWriter(stream);
        }

        private void write(string str)
        {
            ThreadPool.QueueUserWorkItem(str2 =>
            {
                lock(_writer)
                {
                    _writer.WriteLine((string) str2);
                    _writer.Flush();
                }
            }, str);
        }

        public LogLevel minLevel { get { return _minLevel; } }
        public void attach(DateTime time) { write(string.Format("\r\n\r\n***** New session started at {0} *****\r\n\r\n", time)); }
        public void setMetadata(string key, string value) { }
        public void write(LogLine line) { write(line.ToString()); }
        public void Dispose() { _writer.Dispose(); }
    }

    public sealed class MemoryLogListener : ILogListener, IEnumerable<LogLine>
    {
        private readonly Dictionary<string, string> _metadata = new Dictionary<string, string>();
        private const int DEFAULT_NUM_LINES = 1024;
        private readonly LogLevel _minLevel;
        private readonly int _numLines;
        private readonly LogLine[] _lines;
        private int _linenum;
        private int _totalLinesWritten;

        public event Action<LogLine> onLineAdded;

        public MemoryLogListener(LogLevel level) : this(level, DEFAULT_NUM_LINES) { }
        public MemoryLogListener(LogLevel level, int numLines)
        {
            _minLevel = level;
            _numLines = numLines;
            _lines = new LogLine[numLines];
        }

        public LogLevel minLevel { get { return _minLevel; } }
        public Dictionary<string, string> metata { get { return _metadata; } }

        public void attach(DateTime time)
        {
            // The metadata is set, do nothing
        }

        public void write(LogLine line)
        {
            _lines[_linenum++ % _numLines] = line;
            _totalLinesWritten++;
            if (null != onLineAdded)
                onLineAdded(line);
        }

        public void setMetadata(string key, string value)
        {
            _metadata[key] = value;
        }

        public IEnumerator<LogLine> GetEnumerator()
        {
            int line = _totalLinesWritten > _numLines ? _linenum : 0;
            if (_totalLinesWritten == 0)
                yield break;
            do
            {
                yield return _lines[line++];
            } while (line != _linenum);
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public IEnumerable<LogLine> tail(int lines)
        {
            if (lines > _numLines)
                throw new ArgumentOutOfRangeException("lines", "lines must be less than " + _numLines);
            int line = _totalLinesWritten > _numLines ? _linenum : 0;
            if (_totalLinesWritten == 0)
                yield break;
            int lret = 0;
            do
            {
                yield return _lines[line++];
                lret++;
            } while (line != _linenum && lret < lines);
        }
    }
}